ThreadLocal解读

您所在的位置:网站首页 threadlocal 线程复用 ThreadLocal解读

ThreadLocal解读

#ThreadLocal解读| 来源: 网络整理| 查看: 265

处理并发问题有两种思路,一种是使用时间换空间,比如sync同步处理保证同一时间只能有一个线程访问并发资源;还有一种是使用空间换时间,比如ThreadLocal这种,为每个线程开辟空间存放并发资源,保证各线程可以同时访问。本篇主要讲解ThreadLocal使用原理及扩展。

1、ThreadLocal基本使用 public class ThreadLocalTest { private static volatile ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0); public static void main(String[] args) { testThreadLocal(); } private static void testThreadLocal() { threadLocal.set(1); System.out.println(threadLocal.get()); new Thread(()-> System.out.println(threadLocal.get())).start(); } }

这段代码中,TL变量初始值为0,在主线程中将其改变为1,但是新开了一个线程再次获取TL变量时,发现其值仍为初始值0。这就是线程局部变量TL的特性: 在同一线程内对局部变量的改变不影响其它线程内的局部变量的值。

2、ThreadLocal原理解读

由TL的特性,可以主要解读其set,get方法的原理,看下set方法。

public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }

ThreadLocalMap 可以理解为类似Map的一个容器,getMap方法这里返回的是Thread类的threadLocals属性。其key为当前TL对象的引用,值为TL变量的值。这样每个线程操作的其实是自己独立的threadLocals属性里面的TL对象,对其它线程的TL对象自然不影响。

3、ThreadLocal内存泄漏问题

3.1、引用关系 ThreadLocal,ThreadLocalMap,Thread之间的引用关系如下图:

其中黑线代表强引用,蓝线代表弱引用。 如果将threadLocalRef置为null,那么threadLocalObj在gc(垃圾回收)的时候势必被回收(弱引用),这时候entry的key就变为了null。同时存在着这样一条引用链:currentThreadRef->currentThreadObj->threadLocalMap->entry->valueRef->threadLocalValue,导致在垃圾回收的时候进行可达性分析的时候,entry的value可达从而不会被回收掉,但是该value永远不能被访问到(不可能通过为null的key访问到value),这样就造成了内存泄漏。 当然,如果线程执行结束后,threadLocalObj,currentThreadObj都会被回收,因此threadLocalMap,entry也都会被回收掉。可是,在实际使用中我们都是会用线程池去维护我们的线程,为了复用线程是不会结束的,所以ThreadLocal内存泄漏就值得我们关注。 3.2、为什么使用弱引用 假设Entry的key使用强引用,在业务代码中执行threadLocalIRef==null操作,以清理掉threadLocalObj实例的目的,但是因为threadLocalMap的Entry强引用threadLocalObj,因此在gc的时候进行可达性分析,threadLocal依然可达,对threadLocal并不会进行垃圾回收,这样就无法真正达到业务逻辑的目的,出现逻辑错误 3.3、ThreadLocal对内存泄漏的改进 在ThreadLocal的生命周期里,针对ThreadLocal存在的内存泄漏的问题,当调用其set,get,remove方法时,都会清理key为null的脏entry。以set为例:

set private void set(ThreadLocal key, Object value) { Entry[] tab = table; int len = tab.length; //tab长度为2的次方 int i = key.threadLocalHashCode & (len-1); //计算key的hash值 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

在该方法中针对脏entry做了这样的处理: 如果当前table[i]!=null的话说明hash冲突就需要向后环形查找entry插入位置,若在查找过程中遇到脏entry就通过replaceStaleEntry进行处理; 如果当前table[i]==null的话说明新的entry可以在i位置直接插入,但是插入后会调用cleanSomeSlots方法检测并清除脏entry

cleanSomeSlots private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }

该方法从tab的i+1位置开始,一直搜索到i+1+log2(len)。若中途未发现脏entry,则搜索结束,否则调用expungeStaleEntry方法清理脏entry,并往后重新搜索log2(len)个tab位。

expungeStaleEntry private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; //清除当前脏entry tab[staleSlot].value = null; tab[staleSlot] = null; size--; Entry e; int i; //往后环形继续查找,直到遇到table[i]==null时结束 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else {//rehash int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }

该方法清理掉当前脏entry后,并没有闲下来,而是继续向后搜索,若再次遇到脏entry继续将其清理,直到哈希桶(table[i])为null时退出。因此方法执行完的结果为 从当前脏entry(staleSlot)位到返回的i位,这中间所有的脏entry都会被清理。 为避免内存泄漏,推荐使用remove方法移除不用的ThreadLocal变量。

4、ThreadLocal扩展

4.1、InheritableThreadLocal TL的特性是在同一线程内对局部变量的改变不影响其它线程内的局部变量的值,即每个线程里的TL对象都是独立互不影响的。如果希望当前线程的TL对象也能够被子线程使用,可以使用ITL。

使用方法 public class ThreadLocalTest { private static volatile ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0); private static volatile InheritableThreadLocal integerInheritableThreadLocal = new InheritableThreadLocal(); public static void main(String[] args) { //testThreadLocal(); testInheritableThreadLocal(); } private static void testInheritableThreadLocal() { integerInheritableThreadLocal.set(1); System.out.println(integerInheritableThreadLocal.get()); new Thread(()-> System.out.println(integerInheritableThreadLocal.get())).start(); } }

在主线程中将ITL变量设置为1,子线程获取ITL也为1,证明ITL的值被传递到了子线程。

基本原理 ITL继承了TL并重写了getMap,createMap,childValue方法。 public class InheritableThreadLocal extends ThreadLocal { //父线程传递给子线程,这里返回的是父线程ITL对象的引用,应注意线程安全问题 protected T childValue(T parentValue) { return parentValue; } //利用Thread的inheritableThreadLocals变量作为副本变量 ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } //Thread 中 inheritableThreadLocals变量不存在时的初始化动作 void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }

可以看出ITL利用的是Thread的inheritableThreadLocals变量作为线程副本变量保存ITL对象。那么父线程的ITL对象如何传递到子线程的呢?初步猜测是子线程初始化时,将父线程的inheritableThreadLocals属性复制给了子线程。果然在Thread的init()方法中找到如下片段:

if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

注意的是一旦子线程被创建以后,再操作父线程中的ITL变量,那么子线程是不能感知的。因为父线程和子线程还是拥有各自的inheritableThreadLocals,只是在创建子线程的“一刹那”将父线程的inheritableThreadLocals属性复制给子线程,后续两者就没啥关系了(传递引用的情况除外,这时父子线程实际还是共享同一个引用对象)。

4.2、TransmittableThreadLocal JDK的ITL类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的主线程的ThreadLocal值传递到子线程,这里就需要使用到TTL。 使用TTL对分布式跟踪系统 、日志收集系统有很大作用。 传送门:https://github.com/alibaba/transmittable-thread-local

使用方法 public class ThreadLocalTest { private static volatile ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0); private static volatile InheritableThreadLocal integerInheritableThreadLocal = new InheritableThreadLocal(); private static volatile TransmittableThreadLocal transmittableThreadLocal = new TransmittableThreadLocal(); public static void main(String[] args) { // testThreadLocal(); // testInheritableThreadLocal(); testTransmittableThreadLocal(); } private static void testTransmittableThreadLocal() { transmittableThreadLocal.set(1); Runnable ttlRunnable = TtlRunnable.get(()->{ System.out.println(Thread.currentThread().toString()+":"+transmittableThreadLocal.get()); }); ExecutorService executorService = Executors.newFixedThreadPool(2); //开启5个线程 for(int i=0;i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3